Repository navigation
feat(client): map date-time-local and require an offset for zoned date-time - #135
Conversation
…e-time The OpenAPI Format Registry defines date-time-local as an RFC 3339 date-time without a time zone. It fell through to String, and under datetime = :zoned a date-time with no offset decoded as UTC. - Under :zoned, date-time-local maps to Dates.DateTime, a wall clock. Generated :zoned models encode through _encode_zoned, which writes a DateTime with no offset. Under :utc, date-time-local stays String, because DateTime already means a UTC instant there. - The ZonedDateTime decoder requires Z or an offset, as RFC 3339 date-time does. The regex ends with \z, so a trailing newline fails. - CONTRACT_VERSION is 5: generated code imports _encode_zoned.
|
@tanmaykm can we get a review of this PR to support undefined timezones? Currently only Julia OpenAPI generator forces an automatic use of UTC |
|
Thanks for this, and for switching from 1. Avoid the contract bump for modules that don't use The client and server templates import utc = OpenAPI.client(doc) # no datetime option
occursin("_encode_zoned", utc) # true
# emits Runtime.require_contract(5, ...)A contract bump costs downstream users: a regenerated module can't load on older runtimes, so each consumer has to raise its
2. An offset on a wall-clock field is silently shifted, not rejected
# wall: {type: string, format: date-time-local}, datetime = :zoned
_decode(R, JSON.parse("""{"wall":"2026-08-07T15:00:00-04:00"}""")).wall
# 2026-08-07T19:00:00 (expected: DecodeError)The test "an offset on a wall-clock field is rejected" passes only because its schema has a 3. Parameters and top-level bodies send a wall clock as UTC The PR notes this as a limit, but the result is wrong bytes on the wire rather than an unsupported case. A OpenAPI.Runtime._scalar(DateTime(2026, 8, 7, 15)) # "2026-08-07T15:00:00.000Z"The server then reads 15:00 local as 15:00 UTC. The maintainer guide asks generators to fail rather than approximate wire behavior. Please either route these paths through the wall-clock encoder, or have the planner reject 4. Release notes for the runtime change The stricter Minor: the |
|
tanmaykm Thanks for the careful review and the repros. I confirmed all five points. The next push has a change for each.
The same trailing-newline issue applies to schema |
Address the review on JuliaComputing#135. The runtime stays at contract 4. - Emit `_encode_zoned` and `_decode_zoned` into `datetime = :zoned` modules. The runtime and the `:utc` output do not change. - `_decode_zoned` rejects `Z`, offsets, and trailing newlines on a `date-time-local` value. It requires `Z` or an offset on a `date-time` value. Arrays, maps, and optional fields use the same rules. - Revert `OpenAPITimeZonesExt` to the lenient decoder, so existing contract 3 and 4 modules keep their behavior. - Under `:zoned`, planning rejects `date-time-local` in a parameter, a header, or a whole body. The shared runtime writes these as UTC. - Anchor the runtime `Dates.DateTime` decoder with `\z`, so a trailing newline does not decode.
- `_decode_zoned` tries each variant of a union with the zoned decoders. A field with `type: [string, integer]` and `format: date-time-local` now rejects `Z` and offsets instead of reading the value as UTC. - A variant that the value already is goes first, as in the runtime, and a failure reports "value does not match any variant". - Add tests for a union of types and for a map of wall clocks.
|
Suggested release notes for #135 and #136, as discussed in point 4:
|
|
Thanks for the quick turnaround. I re-checked each point against
Two non-blocking points: Module-local codecs on every field. Under Fields vs. parameters. In a new Neither point blocks merging; I'm happy to take them as follow-ups if you'd prefer. |
Yes, I can open new PRs as follow ups to these changes so we can get unblocked and contribute the rest |
tanmaykm
left a comment
There was a problem hiding this comment.
Approving. The remaining points will be handled in follow-up PRs, as discussed above. Thanks!
Summary
The OpenAPI Format Registry
defines
date-time-localas an RFC 3339 date-time without a time zone. OpenAPI.jl did notmap it, so the field became a
String. Underdatetime = :zoned, adate-timewith nooffset decoded as UTC, which RFC 3339 does not allow.
This PR replaces #134 (closed), which proposed a custom
datetime = :as_writtenmode.It uses the registry format instead, so the schema states the form of each timestamp.
Behavior
datetime = :utc(default)datetime = :zoneddate-timeDates.DateTime, offset normalized to UTC (unchanged)ZonedDateTime;Zor an offset is now requireddate-time-localString(unchanged)Dates.DateTime, a wall clock with no offsetanyOfof bothUnder
:utc,Dates.DateTimealready means a UTC instant, sodate-time-localstays aStringthere. Under:zoned,date-timeis aZonedDateTime, so aDates.DateTimecan only mean the wall clock.
Changes
src/planning.jl:format: date-time-localmaps through_local_datetime_type!,dispatched on
Val(datetime).src/client.jl,src/servergen.jl:_emit_modeltakes anencodekeyword. A:zonedmodule encodes model fields, aliases, union values and additional properties through
_encode_zoned.:utcoutput does not change.src/runtime.jl:_encode_zoned(::Dates.DateTime)writesyyyy-mm-ddTHH:MM:SS.ssswith no offset, and recurses into vectors, tuples, dicts and named tuples. Any other
value goes to
_encode.ext/OpenAPITimeZonesExt.jl: theZonedDateTimedecoder requiresZor an offset.Its regex ends with
\z, so a trailing newline is rejected. PCRE$matches beforeone.
CONTRACT_VERSIONis 5, because generated code imports_encode_zoned. Contract 3 and4 modules still load.
clients.md,models.md,artifacts.md,servers.md, and theclientdocstring.
Breaking change for
:zonedusersA
:zonedmodule rejects adate-timevalue with no offset. Before, it read the value asUTC. The existing test that pinned that behavior now asserts a
DecodeError. A producerthat writes zoneless values should declare them
date-time-local.Limits
A
date-time-localvalue sent directly as a query, header or cookie parameter, or as atop-level request body, still goes through
_encode/_scalar, which addZ. Onlymodel values use
_encode_zoned.The release that carries contract 5 needs a version bump. I left that to you.
Tests
test/runtime.jlhas a new test set, "date-time-local is a wall clock under datetime =:zoned". It covers:
:utckeepsString;:zonedfield types for a required, an array and ananyOffield;anyOfbranch selection by schemapattern;The
:zonedtest set now asserts that a zoneless value and a trailing newline arerejected.
test/generated_contract.jlmoves its tripwire to contract 5 and checks thatcontracts 3, 4 and 5 load.
julia --project=. -e 'using Pkg; Pkg.test()': "OpenAPI tests passed", 1442 passes, 0failures, 0 errors.
generated runtime units: 392 of 392.